package pkgmanager

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/ossf/package-analysis/internal/utils"
	"github.com/ossf/package-analysis/pkg/api/pkgecosystem"
)

// npmPackageJSON represents relevant JSON data from the NPM registry response
// when package information is requested.
// See https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
type npmPackageJSON struct {
	DistTags struct {
		Latest string `json:"latest"`
	} `json:"dist-tags"`
}

// npmVersionJSON represents relevant JSON data from the NPM registry response
// when package version information is requested.
// See https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md
type npmVersionJSON struct {
	Dist struct {
		Tarball string `json:"tarball"`
	} `json:"dist"`
}

func getNPMLatest(pkg string) (string, error) {
	resp, err := http.Get(fmt.Sprintf("https://registry.npmjs.org/%s", pkg))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	decoder := json.NewDecoder(resp.Body)
	var details npmPackageJSON
	err = decoder.Decode(&details)
	if err != nil {
		return "", err
	}

	return details.DistTags.Latest, nil
}

/*
getNPMArchiveFilename generates a filename for a package archive to be downloaded from NPM.
It is generated by replacing any '/' characters in the package name with '-' (ref [1]).
Unlike in [1], the leading '@' is not stripped as '@' characters are allowed in filenames.
The cleaned package name is then concatenated with "-", the version string and ".tgz".

[1] https://github.com/npm/cli/blob/8ecbcb9a54b95541f35ebce55d60e4a1feac82c6/lib/commands/pack.js#L64
*/
func getNPMArchiveFilename(pkgName, version, _ string) string {
	cleanedName := strings.ReplaceAll(pkgName, "/", "-")
	return fmt.Sprintf("%s-%s.tgz", cleanedName, version)
}

func getNPMArchiveURL(pkgName, version string) (string, error) {
	resp, err := http.Get(fmt.Sprintf("https://registry.npmjs.org/%s/%s", pkgName, version))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	responseBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("error reading HTTP response: %w", err)
	}

	responseString := string(responseBytes)

	decoder := json.NewDecoder(strings.NewReader(responseString))
	var packageInfo npmVersionJSON
	if err := decoder.Decode(&packageInfo); err != nil {
		// invalid version, non-existent package, etc. Details in responseString
		return "", fmt.Errorf("%w. NPM response: %s", err, responseString)
	}

	return packageInfo.Dist.Tarball, nil
}

var npmPkgManager = PkgManager{
	ecosystem:       pkgecosystem.NPM,
	latestVersion:   getNPMLatest,
	archiveURL:      getNPMArchiveURL,
	archiveFilename: getNPMArchiveFilename,
	extractArchive:  utils.ExtractArchiveFile,
}
